4-4 RBAC角色权限实现:创建角色权限装饰器
概述
本节完成两件事:一是完善数据库模型(添加 Permission 和 RolePermission),二是创建自定义装饰器为 Controller 和 Route 打上唯一权限标识。核心难点在于装饰器的元数据累加机制——当同一个方法上使用多个装饰器时,需要将它们累积到数组中而非互相覆盖。
Permission 数据模型
模型设计
// schema.prisma
model Permission {
id Int @id @default(autoincrement())
name String @unique // 模块标识 + 路由标识,如 "user:create"
action String // 操作类型:read, create, update, delete, manager
rolePermissions RolePermission[]
@@map("permissions")
}
model RolePermission {
roleId Int @map("role_id")
permissionId Int @map("permission_id")
role Role @relation(fields: [roleId], references: [id])
permission Permission @relation(fields: [permissionId], references: [id])
@@id([roleId, permissionId])
@@map("role_permissions")
}
prisma
Permission 字段说明
| 字段 | 类型 | 说明 | 示例 |
|---|---|---|---|
name | String (@unique) | 模块标识与路由标识的组合 | "user:create", "role:read" |
action | String | 操作类型枚举 | read, create, update, delete, manager |
name 的设计原则:不是真正的 Controller/Route 名称,而是通过装饰器人为指定的唯一字符串标识。这样即使后续修改了方法名或路由名,只要标识不变,权限就不会失效。
完整模型关系图
User ──(多对多)── Role ──(一对多)── RolePermission ──(多对一)── Permission
UserRole rolePermissions permission
text
Action 枚举定义
// common/enums/actions.enum.ts
export enum Action {
Manager = 'manager', // 拥有所有权限
Create = 'create',
Read = 'read',
Update = 'update',
Delete = 'delete',
}
typescript
权限装饰器实现
基础装饰器
使用 SetMetadata 将权限标识存储到路由的元数据中:
// common/decorators/role-permission.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const PERMISSION_KEY = 'permission';
// 通用权限装饰器
export const Permission = (permission: string) =>
SetMetadata(PERMISSION_KEY, permission);
typescript
便捷装饰器(CRUD 快捷方式)
对于标准 CRUD 操作,提供无需参数的快捷装饰器:
export const Create = () =>
SetMetadata(PERMISSION_KEY, Action.Create.toLowerCase());
export const Read = () =>
SetMetadata(PERMISSION_KEY, Action.Read.toLowerCase());
export const Update = () =>
SetMetadata(PERMISSION_KEY, Action.Update.toLowerCase());
export const Delete = () =>
SetMetadata(PERMISSION_KEY, Action.Delete.toLowerCase());
typescript
装饰器元数据累加问题
问题:多个装饰器互相覆盖
当在同一个方法上使用多个装饰器时,后一个会覆盖前一个:
// ❌ 问题:只有最后一个 @Update 生效
@Read()
@Update()
test() {}
typescript
解决方案:accumulateMetadata 累加函数
核心思路:使用 Reflect.getMetadata 读取已有元数据,与新值合并后再 SetMetadata。
// common/decorators/role-permission.decorator.ts
import { SetMetadata } from '@nestjs/common';
import 'reflect-metadata';
export const PERMISSION_KEY = 'permission';
// 累加元数据的工厂函数
const accumulateMetadata = (key: string, permission: string) => {
return (
target: any,
propertyKey?: string | symbol,
descriptor?: TypedPropertyDescriptor<any>,
) => {
if (descriptor && descriptor.value) {
// 方法级装饰器:从 descriptor.value 读取已有元数据
const existingPermissions =
Reflect.getMetadata(key, descriptor.value) || [];
const newPermissions = [...existingPermissions, permission];
SetMetadata(key, newPermissions)(target, propertyKey, descriptor);
} else {
// 类级装饰器:从 target 读取已有元数据
const existingPermissions = Reflect.getMetadata(key, target) || [];
const newPermissions = [...existingPermissions, permission];
SetMetadata(key, newPermissions)(target);
}
};
};
// 支持累加的通用权限装饰器
export const Permission = (permission: string) =>
accumulateMetadata(PERMISSION_KEY, permission);
// 便捷 CRUD 装饰器(同样支持累加)
export const Create = () => accumulateMetadata(PERMISSION_KEY, Action.Create);
export const Read = () => accumulateMetadata(PERMISSION_KEY, Action.Read);
export const Update = () => accumulateMetadata(PERMISSION_KEY, Action.Update);
export const Delete = () => accumulateMetadata(PERMISSION_KEY, Action.Delete);
typescript
累加机制原理
第一次装饰器: metadata = ["read"]
第二次装饰器: metadata = ["read", "update"] ← 累加而非覆盖
第三次装饰器: metadata = ["read", "update", "user1"]
text
Controller 中的使用示例
// user.controller.ts
import { Controller, Get, Put, UseGuards } from '@nestjs/common';
import { Permission, Read, Update } from '../common/decorators/role-permission.decorator';
import { RolePermissionGuard } from '../common/guards/role-permission.guard';
@Controller('user')
@Permission('user') // 类级标识 → classPermission = "user"
@UseGuards(RolePermissionGuard)
export class UserController {
@Get()
@Read() // 方法级标识 → handlerPermission = "read"
findAll() {
return this.userService.findAll();
}
@Put(':id')
@Update() // 方法级标识 → handlerPermission = "update"
update() {
return this.userService.update();
}
@Get('test')
@Read()
@Update() // 累加 → handlerPermission = ["read", "update"]
test() {
return { message: 'test' };
}
}
typescript
Guard 读取元数据
// common/guards/role-permission.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSION_KEY } from '../decorators/role-permission.decorator';
@Injectable()
export class RolePermissionGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 读取方法级装饰器
const handlerPermission = this.reflector.get(
PERMISSION_KEY,
context.getHandler(),
);
// 读取类级装饰器
const classPermission = this.reflector.get(
PERMISSION_KEY,
context.getClass(),
);
console.log('classPermission:', classPermission); // "user"
console.log('handlerPermission:', handlerPermission); // "read" 或 ["read", "update"]
return true; // 后续实现权限比对逻辑
}
}
typescript
装饰器设计的核心价值
| 价值 | 说明 |
|---|---|
| 唯一性 | 通过人为指定的字符串确保权限标识全局唯一 |
| 解耦 | 标识与代码结构无关,修改方法名不影响权限 |
| 可读性 | @Permission('user') + @Read() 即表示 user:read 权限 |
| 累加性 | 同一方法可叠加多个权限标识,支持多权限路由 |
下一步
装饰器和 Guard 的元数据读取部分已完成,接下来需要实现:从数据库查询用户的角色和权限,与装饰器中的标识进行匹配。
↑